iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
Modern Web

起步Go!Let's Go!系列 第 13

[ Day 13] Go 結構魔法:定義、實體化、編織

  • 分享至 

  • xImage
  •  

Struct 結構

結構 (Struct) 是用來存放其他資料欄位的容器。
Struct 在 Go 語言中常用來描述一個複雜的資料結構,
簡單來說一個結構中會包含有很多資料,並將其綜合起來。
在 Go 中要使用結構有兩個步驟:

  1. 定義
  2. 實體化

要先定義結構,然後才能實體化結構。

定義結構

Struct 是一種複合型態,可以在裡面存放不同型態的變數。

基本語法:
:::info
type 結構名稱 struct{
欄位名稱 資料型態
欄位名稱 資料型態
.....
}
:::
這邊的 struct 並不是變數,而是一個自創的型態,也可以自行命名自創型態的名稱。

type Point struct{
    x int
    y int
}

也可以放不同資料型態的資料進去。

type person struct{
    name string
    age uint
}

剖析 type

我們可以利用 type 創建新的「型態別名」,那什麼是「型態別名」?簡單來說,就是讓相同的型態具有不同的名稱。
舉例來說,rune 就是 int32 的別名,因此可以使用 rune 來代表 int32 型態的值。
另外也可以自己也可以使用 type 定義自己的型態別名。
例如:

package main
import "fmt"
type Celsius float64
type Fahrenheit float64

這裡定義了 Celsius 和 Fahrenheit 兩個型態別名,它們分別表示攝氏度和華氏度的溫度值。
這樣我就可以運用這個型態別名宣告新的資料型態,也可以讓其程式碼更易讀且更具可讀性。

package main
import "fmt"
type Celsius float64
type Fahrenheit float64

func main(){
    var c Celsius = 44.5
    f := celsiusToFahrenheit(c)
    fmt.Println(f)
}
func celsiusToFahrenheit(c Celsius) Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

執行結果
112.1

上面定義了一個 celsiusToFahrenheit 函式,接收一個 Celsius 型態的參數 c,並回傳一個 Fahrenheit 型態的值。

實體化結構&宣告 struct 型態的變數

給定資料,並產生結構的實體。
基本語法:
:::info
第一種寫法:
結構名稱 { 欄位資料, 欄位資料, ...... }

第二種寫法:
這個就不用在意順序。
結構名稱 { 欄位名稱: 資料, 欄位名稱: 資料, ...... }
:::

package main
import "fmt"
type Point struct {
    x int
    y int
}
func main(){
    // 宣告 p1, p2 的資料型態為 Point
    var p1 Point = Point {3, 4}      // 第一種寫法
    var p2 Point = Point {y:3, x:1}  // 第二種寫法
}

定義一個結構,並產生以下兩個結構實體。
宣告一個變數名稱 p1 並給一個資料型態 Point,並將 Point 實體化,3 就會對應到 x4 就會對應到 y
宣告一個變數名稱 p2 並給一個資料型態 Point,欄位 y 的資料為 2,欄位 x 的資料為 1

宣告一個 person 型態的變數:

package main
import "fmt"
type person struct{
    name string
    age uint
}

func main(){
    var someone person
    someone.name = "Tom"
    someone.age = 27
    fmt.Println(someone)
    fmt.Printf("%T", someone)
}

執行結果
{Tom 27}
main.person

型態為 person,但為什麼會印出 main.person 呢?
%Tfmt 套件中的一個轉換字元,用來輸出一個值的型態。會印出 main.person,是因它是在 package main 中宣告的,所以型態的完整表示法是 main.person。因此當你印出 someone 的型態時,會印出 main.person

簡化宣告

除了用上面宣告的方式,也可以將其簡化。

package main
import "fmt"
type person struct{
    name string
    age uint
}

func main(){
    someone := person{
        name: "Tom"
        age: 27
    }
    fmt.Println(someome)
    fmt.Printf("%T\n", someone)
}

執行結果
{Tom 27}
main.person

不用 type 直接使用 struct 宣告

package main
import "fmt"

func main(){
    someone := struct{
        name string
        age uint
    }{
        name: "Tom",
        age: 27,
    }
    fmt.Println(someone)
    fmt.Printf("%T", someone)
}

執行結果
{Tom 27}
struct { name string; age uint }

這是一個匿名結構,它包含一個 name 字串和一個 age 以無號整數 (uint) 表示的年齡。匿名結構是一種結構,但它沒有名稱,因此不能被其他程式碼重複使用。通常在只需要一個簡單的結構並且不需要在其他地方重複使用它時使用匿名結構。

欄位中的資料

產生結構實體後,真正需要的是結構實體中欄位的資料。
會使用 . 存取欄位中的資料。
基本語法:
:::info
結構實體.欄位名稱
:::

type Point struct {
    x int
    y int
}

func main(){
    var p1 Point = Point {3, 4}
    fmt.Println(p1.x, p1.y)
}

p1.x 的資料就是 3
p1.y 的資料就是 4

結構觀念總覽

先訂一個自己定義的 (Point) 結構 (struct),它是一個新的資料型態 (type),並給定資料欄位。
在主程式中將結構實體化,然後取得內部欄位的資料。
p1.x 對應到的就會是 3,p1.y 對應到的就會是 4。
p2.x 對應到的就會是 1,p2.y 對應到的就會是 2。
左邊圖:
在概念上只有一個結構定義,我們可以根據結構定義去做出兩個結構實體。
主程式的第一行去產生一個結構實體,欄位 x:3 y:4 放進變數 p1 中。
主程式的第二行去產生一個結構實體,欄位 x:1 y:2 放進變數 p2 中。

資料型態

結構會是一種資料型態,例如範例中的 Point 結構。

練習

package main
import "fmt"
// 定義一個新的結構
type Person struct {
    name string
    age  int
}

func main() {
    var p1 Person = Person{"Dylan", 20}
    fmt.Println(p1.name, p1.age) // Dylan 20
    var p2 Person = Person{age: 25, name: "John"}
    fmt.Println(p2.name, p2.age) // John 25
}

在定義結構時,會定義在 Function 外。
先定義一個 Person 的結構,裡面有 name 跟 age 欄位,資料型態分別是 string 跟 int。
接著來使用結構,並將結構實體化。
所以宣告一個 p1 變數,資料型態就會是剛剛定義的結構名稱,實體化結構並把資料依序寫進去,也就是 name 跟 age,這樣就產生了一個 p1 的結構實體。
宣告 p2 變數,這時明確的指定資料名稱給 p2,並產生一個 p2 的結構實體。

如果要改名字,可以直接給新的資料,用 p2.name= 的方式去覆蓋原本的資料,如下:

package main
import "fmt"
type Person struct {
    name string
    age  int
}

func main() {
    var p1 Person = Person{"Dylan", 20}
    fmt.Println(p1.name, p1.age) // Dylan 20
    var p2 Person = Person{age: 25, name: "John"}
    p2.name = "Tom"
    fmt.Println(p2.name, p2.age) // Tom 25
}

要怎麼在函示中用 Struct?

先來個範例:

package main
import "fmt"
type Person struct {
    name string
    age  uint
}

func plusOne(p person){
    p.age = p.age + 1
}
func main(){
    someone := person{
        name: "Tom"
        age: 27
    }
    fmt.Println(someone)
    plusOne(someone)
    fmt.Println(someone)
}

執行結果
{Tom 27}
{Tom 27}

不曉得大家有沒有發現原因,給個小提示,這跟指標有關!!!
這是因為 someone 是一個 person 型態變數,並不是指標變數,如果要在函示中更改 someone 的值,那就要先取得 someone 的指標,才能更改 someone 的值。

利用指標 ( pointer ) 取得 struct{}

上面說要先取得 struct 的指標,要怎麼取得呢?
如果指標有學好,這就難不倒你了。

package main
import "fmt"

type person struct{
    name string
    age uint
}

func main(){
    someone := person{
        name: "Tom",
        age: 27,
    }
    fmt.Println(someone)
    plusOne(&someone)    // 利用 & 取得 someone 的記憶體位址
    fmt.Println(someone)
}

func plusOne(p *person){    //記得改成指標的資料型態 *person
    (*p).age = (*p).age + 1
    fmt.Println("The age of %s is %d\n", (*p).name, (*p).age)
}

執行結果:
{Tom 27}
The age of Tom is 28
{Tom 28}

第 20 行的寫法有點不太像工程師懶人的寫法,有沒有更簡單的寫法呢?
當然是有的,前面使用 struct{} 的變數是用 . 來取值的。當使用指標取得 struct 的值時,可以使用「指標運算子」* 取得指標所指向的值。而在取得 struct 的值時,編譯器會自動轉換成指標操作,因此可以直接使用 . 取得該 struct 的欄位值。
所以第 20 行可以改成這樣:

(*p).age = (*p).age + 1 // before
// 改成
p.age = p.age + 1 // after
package main
import "fmt"

type person struct{
    name string
    age uint
}

func main(){
    someone := person{
        name: "Tom",
        age: 27,
    }
    fmt.Println(someone)
    plusOne(&someone)    // 利用 & 取得 someone 的記憶體位址
    fmt.Println(someone)
}

func plusOne(p *person){    //記得改成指標的資料型態 *person
    p.age = p.age + 1
    fmt.Println("The age of %s is %d\n", (*p).name, (*p).age)
}

執行結果:
{Tom 27}
The age of Tom is 28
{Tom 28}

宣告時用 new() 指向 struct{} 的指標變數

除了之前的宣告方式,在 Go 中可以用 new() 宣告指向 struct{} 的指標變數。

package main
import "fmt"

type person struct{
    name string
    age uint
}

func main(){
    someone := new(person)
    someone.name = "Tom"
    someone.age = 27
    fmt.Println(someone)
    fmt.Println(*someone)
    fmt.Println(&someone)
}

執行結果
&{Tom 27}
{Tom 27}
0x140000ae020

全域變數

在講解迴圈時有提到 scope 的概念,然而將變數宣告在函示外,那它的 scope 會相當大,可以讓 Go 裡面的所有函示都能直接使用,而這種變數稱之為全域變數

package main
import "fmt"

var n int

func double(){
    n = n * 2
}

func main(){
    n = 5
    fmt.Println(n)
    double()
    fmt.Println(n)
}

執行結果
4
8

上面的例子,我們可在 func main() 外先宣告變數 x,這時候 x 就是一個全域變數,x 不但可以使用在 main(),其他的函式也可以使用 x 變數,且不需傳遞。

也可以這樣宣告

package main
import "fmt"

var n = 5

func double(){
    n = n * 2
}

func main(){
    fmt.Println(n)
    double()
    fmt.Println(n)
}

執行結果
5
10

這邊的 var n = 5,跟 n := 5 是一樣的,當然也可以寫成 var n int = 5,取決於個人習慣去使用。
:::warning
注意!!!!!
如果要將變數變成全域變數就不能用 :=
:::

package main
import "fmt"

n := 5

func double(){
    n = n * 2
}

func main(){
    fmt.Println(n)
    double()
    fmt.Println(n)
}

執行結果
syntax error: non-declaration statement outside function body (exit status 1)

因為 Go 規定不可以再函式外使用 := 這種短變數宣告,必須使用 varconst 來宣告全域變數。


上一篇
[ Day 12] Go 指標參數:釋放函式的潛力
下一篇
[ Day 14 ] Go 陣列:創建、賦能、巡禮
系列文
起步Go!Let's Go!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言